# -*- coding: utf-8 -*-
# @Time    : 2020/8/22 9:28 上午
# @Author  : zhongxin
# @Email   : 490336534@qq.com
# @File    : elementoperator.py
# @Desc    : 元素对象操作
import base64
import copy
import json
import os
import platform
import random
import socket
import time

import allure
import yaml
from PIL import Image
from appium.webdriver.common.mobileby import MobileBy
from selenium import webdriver
from appium import webdriver as app_webdriver
from selenium.common.exceptions import WebDriverException, NoSuchElementException
from selenium.webdriver import DesiredCapabilities, ActionChains
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait
from urllib3.exceptions import ReadTimeoutError

from src.utils.allureoperator import attach_png
from src.utils.constant import TEST_PIC, TOOL_PATH, HEADLESS, REPORT_PATH, REMOTE_URL, REMOTE_PORT, BASE_DATA_PATH
from src.utils.cvtools import imread
from src.utils.logoperator import LogOperator
from src.utils.picoperator import Template

logger = LogOperator(__name__)


class Locator:
    """
    页面元素封装
    """

    def __init__(self, element, wait_sec=3, by_type='id', locator_name='', desc=''):
        """

        @param element: 定位语句
        @param wait_sec: 等待时间 默认3秒
        @param by_type: 定位方式
        @param locator_name: 变量名
        @param desc: 描述
        """
        self.element = element
        self.wait_sec = wait_sec
        self.by_type = by_type
        self.locator_name = locator_name
        self.desc = desc

    def __str__(self):
        return f'{self.desc}:(By:{self.by_type},element:{self.element})'

    def __repr__(self):
        return f'{self.desc}'


class ElementOperator:
    """
    元素操作
    """

    def __init__(self, path=None, file_name=None, driver=None):
        """

        :param path: 元素定位yaml文件
        :param file_name: 页面名
        :param driver: 浏览器对象
        """
        self.driver = driver
        self.time_out = 10
        self.path = path
        self.url = '/'
        self.cookie_file = f'{REPORT_PATH}/cookies.json'
        self.remote_url = f"{REMOTE_URL}:{REMOTE_PORT}/wd/hub"
        self.file_name = file_name or os.path.splitext(os.path.split(path)[-1])[0]
        self.data_dict = self._parse_yaml()
        self._locator_map = self.read_yaml()

    def _parse_yaml(self):
        """
        读取Yaml文件内容
        :return:
        """
        data_dict = {}
        try:
            with open(self.path, 'r+', encoding='utf-8') as f:
                data_dict = yaml.load(f, Loader=yaml.FullLoader) or {}
        except Exception as e:
            raise Exception(e)
        finally:
            return data_dict

    def read_yaml(self):
        """
        页面元素定位
        :return:
        """
        pages_list = self.data_dict["pages"]
        locator_map = dict()
        for page in pages_list:
            page_name = page["page"]["pageName"]
            page_desc = page["page"]["desc"]
            locator_map[page_name] = dict()
            locators_list = page["page"]["locators"]
            for locator in locators_list:
                by_type = locator["type"]
                element = locator["value"]
                wait_sec = int(locator.get("timeout", 3))
                locator_name = locator["name"]
                desc = f"{page_desc}_{locator['desc']}"
                tmp = Locator(element, wait_sec, by_type, locator_name, desc)
                locator_map[page_name][locator_name] = tmp
        return locator_map

    def __getattr__(self, item):
        if "_locator_map" in dir(self) and "file_name" in dir(self):
            if item in self._locator_map[self.file_name]:
                locator = self.get_locator(item)
                if locator:
                    return locator
                else:
                    return self[item]

    def create_code(self):
        for k, v in self.read_yaml()[self.file_name].items():
            logger.info(f'''
            @property
            def {k}(self):
                """
                {v}
                """
                return self.get_locator("{k}")
            ''')

    def open(self, url, locator, frame_locator=None, driver='chrome', deviceName='iPhone X'):
        """

        :param url: 打开的地址
        :param locator: 确认打开成功的元素
        :param frame_locator: 需要切换的frame
        :param driver: 浏览器驱动
        :param deviceName: h5测试设备型号
        :return:
        """
        driver = driver.lower()
        if driver in ['chrome', 'ie', 'chrome-h5']:
            try:
                socket.setdefaulttimeout(50)
                if not self.driver:
                    if 'chrome' in driver:
                        desired_capabilities = DesiredCapabilities.CHROME
                        desired_capabilities["pageLoadStrategy"] = "eager"
                        chrome_option = Options()
                        if HEADLESS == 'true':
                            chrome_option.add_argument('--headless')
                        if driver == 'chrome-h5':
                            chrome_option.add_experimental_option('mobileEmulation', {'deviceName': deviceName})
                            chrome_option.add_experimental_option('w3c', False)
                        if platform.system() == "Windows":
                            driver_path = f'{TOOL_PATH}/driver/win/chromedriver'
                        elif platform.system() == "Linux":
                            chrome_option.add_argument('--headless')
                            chrome_option.add_argument('--no-sandbox')
                            chrome_option.add_argument('--disable-gpu')
                            chrome_option.add_argument('--disable-dev-shm-usage')
                            driver_path = f'{TOOL_PATH}/driver/linux/chromedriver'
                            os.system(f"chmod -R 777 {driver_path}")
                        else:
                            driver_path = f'{TOOL_PATH}/driver/mac/chromedriver'
                            os.system(f"chmod -R 777 {driver_path}")
                        logger.info(f'使用的chromedriver为「{driver_path}」。请确认版本是否与当前浏览器版本一致')
                        self.driver = webdriver.Chrome(executable_path=f'{driver_path}',
                                                       chrome_options=chrome_option,
                                                       desired_capabilities=desired_capabilities)
                    elif driver == 'ie':
                        ie_options = DesiredCapabilities.INTERNETEXPLORER  # 将忽略IE保护模式的参数设置为True
                        ie_options['ignoreProtectedModeSettings'] = True  # 忽略浏览器缩放设置
                        ie_options['ignoreZoomSetting'] = True  # 启动带有自定义设置的IE浏览器
                        self.driver = webdriver.Ie(capabilities=ie_options)
                time.sleep(2)
                self.driver.get(url)
                time.sleep(5)
                try:
                    self.driver.maximize_window()
                except Exception as e:
                    logger.error(f"浏览器最大化失败:{e}")
                if frame_locator:
                    self.wait_element_visible(frame_locator)
                    self.switch_frame(frame_locator)
                if locator:
                    self.wait_element_visible(locator)
            except Exception as e:
                raise Exception(f"打开浏览器进入{url}失败:{e}")
        self.wait_for(10)
        return self.driver

    def get_desired_caps(self, sys, udid, app):
        desired_caps = {"udid": udid}
        if sys == 'android':
            desired_caps['platformName'] = "Android"
            if app == 'wys':
                # 打开微医生APP
                desired_caps["appPackage"] = "com.greenline.yihuantong"
                desired_caps["appActivity"] = ".home.WelcomeActivity"
            elif app == 'wy':
                # 打开微医APP
                desired_caps["appPackage"] = "com.greenline.guahao"
                desired_caps["appActivity"] = ".home.WelcomeActivity"
            desired_caps["skipServerInstallation"] = True
            desired_caps["automationName"] = "UiAutomator2"
            desired_caps["noReset"] = True
            desired_caps["newCommandTimeout"] = 3600
        elif sys == 'ios':
            desired_caps['platformName'] = "iOS"
            if app == 'wys':
                # 打开微医生APP
                desired_caps["bundleId"] = "com.minkanginfo.guahao"
            elif app == 'wy':
                # 打开微医APP
                desired_caps["bundleId"] = "com.lvxian.guahao"
            desired_caps["automationName"] = "XCUITest"
            desired_caps["deviceName"] = 'iphone'

        return desired_caps

    def open_phone(self, sys='android', udid="039acad40405", app='wys'):
        """
        移动端打开操作
        http://appium.io/docs/en/writing-running-appium/caps/
        """
        desired_caps = self.get_desired_caps(sys, udid, app)
        logger.info(desired_caps)
        self.driver = app_webdriver.Remote(self.remote_url, desired_caps)
        self.wait_for(10)
        return self.driver

    def install_apk(self, sys='android', udid="039acad40405", app='wys', apk=None):
        desired_caps = self.get_desired_caps(sys, udid, app)
        if apk:
            desired_caps1 = copy.deepcopy(desired_caps)
            desired_caps1["appPackage"] = "com.android.settings"
            desired_caps1["appActivity"] = ".Settings"
            driver = app_webdriver.Remote(self.remote_url, desired_caps1)
            if app == 'wys':
                self._install_apk(driver, "com.greenline.yihuantong", apk)
            elif app == 'wy':
                self._install_apk(driver, "com.greenline.guahao", apk)
            driver.quit()

    def _install_apk(self, driver, app_name, apk):
        """
        删除app_name包后重新安装apk
        """
        self.remove_app(driver, app_name)
        driver.install_app(apk, grantPermissions=True, time_out=300)

    def remove_app(self, driver, app_name):
        """
        卸载app_name
        """
        if driver.is_app_installed(app_name):
            driver.remove_app(app_name)

    def close(self):
        if self.driver:
            try:
                self.driver.close()
            except Exception as e:
                logger.error(f"close浏览器失败：{e}")
            try:
                self.driver.quit()
            except Exception as e:
                logger.error(f"quit浏览器失败：{e}")
            finally:
                self.driver = None

    def get_locator(self, locator_name):
        locator = self._locator_map.get(self.file_name)
        if locator:
            locator = locator.get(locator_name)
        return locator

    def wait_for(self, wait_sec):
        self.driver.implicitly_wait(wait_sec)

    def _wait_page_load(self):
        try:
            WebDriverWait(self.driver, self.time_out).until(lambda d: d.execute_script("return document.readyState"))
        except Exception as e:
            raise Exception(e)

    def wait_element_visible(self, locator):
        """
        检查元素是否存在
        :param locator:
        :return:
        """
        locator = self._get_locator_tuple(locator)
        ele = WebDriverWait(self.driver, self.time_out).until(
            expected_conditions.visibility_of_element_located(locator))
        return ele

    @staticmethod
    def _get_locator_tuple(locator):
        type_dict = {
            "id": By.ID,
            "xpath": By.XPATH,
            "link_text": By.LINK_TEXT,
            "partial_link_text": By.PARTIAL_LINK_TEXT,
            "name": By.NAME,
            "tag_name": By.TAG_NAME,
            "class_name": By.CLASS_NAME,
            "css_selector": By.CSS_SELECTOR,
            "ios_predicate": MobileBy.IOS_PREDICATE,
            "ios_uiautomation": MobileBy.IOS_UIAUTOMATION,
            "ios_class_chain": MobileBy.IOS_CLASS_CHAIN,
            "android_uiautomator": MobileBy.ANDROID_UIAUTOMATOR,
            "android_viewtag": MobileBy.ANDROID_VIEWTAG,
            "android_data_matcher": MobileBy.ANDROID_DATA_MATCHER,
            "android_view_matcher": MobileBy.ANDROID_VIEW_MATCHER,
            "windows_ui_automation": MobileBy.WINDOWS_UI_AUTOMATION,
            "accessibility_id": MobileBy.ACCESSIBILITY_ID,
            "image": MobileBy.IMAGE,
            "custom": MobileBy.CUSTOM,
            "mobile_xpath": MobileBy.XPATH,
            "Template": Template
        }
        locator_t = (type_dict[locator.by_type], locator.element)
        return locator_t

    def switch_frame(self, frame_locator):
        self.driver.switch_to.default_content()
        self.driver.switch_to.frame(frame_locator.element)

    def find_element(self, locator):
        self.wait_for(locator.wait_sec)
        web_ele = self._get_element(locator)
        return web_ele

    def find_elements(self, locator):
        self.wait_for(locator.wait_sec)
        web_eles = self._get_elements(locator)
        return web_eles

    def height_light(self, element):
        """
        元素高亮
        :param element:
        :return:
        """
        self.driver.execute_script("arguments[0].setAttribute('style',arguments[1]);",
                                   element, "border:2px solid red;")

    def loop_find(self, query):
        file_name = os.path.join(TEST_PIC, f'{str(int(time.time() * 1000))}.png')
        self.screenshot_pic(file_name)
        screen = imread(file_name)
        if screen is None:
            logger.warning("截图失败")
        else:
            match_pos = query.match_in(screen)
            if match_pos:
                return match_pos

    def _get_element(self, locator):
        start_time = time.time()
        time_out = locator.wait_sec
        while True:
            try:
                value_text = locator.desc
            except:
                value_text = ""
            try:
                locator_t = self._get_locator_tuple(locator)
                if locator_t[0] != Template:
                    web_element = self.driver.find_element(*locator_t)
                else:
                    logger.info("使用图片比对算法进行图片坐标查找")
                    web_element = self.loop_find(Template(**eval(locator_t[1])))
                    if web_element is None:
                        error_msg = f'当前没有匹配到「{value_text}」图片对应的元素'
                        logger.error(error_msg)
                        raise NoSuchElementException(error_msg)
                try:
                    self.height_light(web_element)
                except Exception:
                    pass
                return web_element
            except NoSuchElementException as n:
                time.sleep(0.5)
                if time.time() - start_time >= time_out:
                    raise NoSuchElementException(f"{time_out}秒后仍没有找到元素「{value_text}」")
            except WebDriverException as w:
                time.sleep(0.5)
                if time.time() - start_time >= time_out:
                    raise WebDriverException(f"{time_out}秒后浏览器仍异常「{value_text}」:{w}")
            except Exception as e:
                raise Exception(f"查找元素异常：{e}")

    def _get_elements(self, locator):
        locator_t = self._get_locator_tuple(locator)
        web_element = self.driver.find_elements(*locator_t)
        return web_element

    def screenshot_pic(self, file_name):
        """
        截图
        @param file_name:
        @return:
        """
        self.driver.save_screenshot(file_name)
        return file_name

    @allure.step("查看「{locator}」是否存在")
    def has_element(self, locator):
        ret = False
        try:
            ele = self.find_element(locator)
            if ele:
                ret = True
        except Exception as e:
            logger.error(f"查看元素「{locator}」是否存在异常:{e}")
        return ret

    @allure.step("往「{locator}」输入「{msg}」")
    def input(self, locator, msg, many=False, num=0, clear=True):
        if many:
            ele = self.find_elements(locator)[num]
        else:
            ele = self.find_element(locator)
        logger.info(f"往「{locator.desc}」输入「{msg}」")
        if clear:
            ele.clear()
        time.sleep(0.2)
        try:
            ele.send_keys(msg)
        except Exception as e:
            logger.error(f"往「{locator}」输入「{msg}」失败:{e}")
        time.sleep(0.2)

    @allure.step("往「{locator}」输入「{key}」")
    def send_keys(self, locator, key):
        """

        :param locator: 元素对象
        :param key:
            * BACK_SPACE 后退
            * SPACE 空格
            * ENTER 回车
        :return:
        """
        ele = self.find_element(locator)
        ele.send_keys(getattr(Keys, key.upper()))
        time.sleep(0.2)

    @allure.step("点击「{locator}」")
    def click(self, locator, many=False, num=0, special=0):
        if many:
            ele = self.find_elements(locator)[num]
        else:
            ele = self.find_element(locator)
        if not isinstance(self.driver, app_webdriver.Remote):
            logger.info(f"在地址：「{self.get_url()}」点击「{locator.desc}」")
        if isinstance(ele, tuple):
            if isinstance(self.driver, app_webdriver.Remote):
                self.driver.tap([ele], 500)
            else:
                ActionChains(self.driver).move_by_offset(*ele).click().perform()
        else:
            if special == 0:
                ele.click()
            elif special == 1:
                webdriver.ActionChains(self.driver).move_to_element(ele).click(ele).perform()
            elif special == 2:
                self.driver.execute_script("arguments[0].click();", ele)
        time.sleep(0.2)
        if not isinstance(self.driver, app_webdriver.Remote):
            logger.info(f"点击后地址为「{self.get_url()}」")

    @allure.step("获取「{locator}」的文字")
    def get_text(self, locator, many=False, num=0):
        if many and num == -1:
            return [i.text for i in self.find_elements(locator)]
        if many:
            ele = self.find_elements(locator)[num]
        else:
            ele = self.find_element(locator)
        logger.info(f"获取「{locator.desc}」")
        return ele.text

    @allure.step("查找文案「{text}」")
    def is_text_exists(self, text, sys='android'):
        page_source = self.driver.page_source
        if len(text) == 0:
            return False
        elif text in page_source:
            if sys == 'ios':
                expect_ele = Locator('//XCUIElementTypeStaticText[@name="' + text + '"]',
                                     by_type="mobile_xpath",
                                     desc="期望出现的文案")
                if self.get_attribute(expect_ele, 'visible') == "true":
                    logger.info(f"找到了文案[{text}]")
                    return True
            elif sys == 'android':
                logger.info(f"找到了文案[{text}]")
                return True
        else:
            logger.info(f"未找到文案[{text}]")
            return False

    @allure.step("等待文案「{text}」出现")
    def wait_page(self, text, need_close_dialog=False, sys='android', source=''):
        loop_time = 3
        flag = self.is_text_exists(text, sys=sys)
        while (not flag) and loop_time > 0:
            if need_close_dialog:
                self.close_dialog(retrytime=1, expect_text='', sys=sys, source=source)
            flag = self.is_text_exists(text, sys=sys)
            time.sleep(3)
            loop_time = loop_time - 1

    @allure.step("刷新页面")
    def refresh(self):
        """
        刷新页面
        :return:
        """
        self.driver.refresh()
        time.sleep(5)

    @allure.step("滚动到{num}位置")
    def scroll_to(self, num):
        """
        下拉至一定位置
        :param num:下拉的位置
        :return:
        """
        js = f'window.scrollTo(0,{100 * num})'
        self.driver.execute_script(js)

    @allure.step("回到上一页")
    def back(self):
        """
        返回上一页
        :return:
        """
        self.driver.back()

    @allure.step("回到下一页")
    def forward(self):
        """
        返回下一页
        :return:
        """
        self.driver.forward()

    @allure.step("获取cookie")
    def get_cookies(self):
        cookies = self.driver.get_cookies()
        try:
            with open(self.cookie_file, 'w') as f:
                f.write(json.dumps(cookies))
        except Exception as e:
            logger.error(f'将cookie存入文件异常:{e}')
        return cookies

    @allure.step("添加cookie")
    def add_cookie(self, cookie_dict=None, type='file'):
        if type == "file":
            with open(self.cookie_file, 'r', encoding='utf8') as f:
                list_cookies = json.loads(f.read())
            for cookie in list_cookies:
                self.driver.add_cookie(cookie)
        else:
            self.driver.add_cookie(cookie_dict)

    @allure.step("清除cookie")
    def delete_all_cookies(self):
        """
        清除全部cookie
        :return:
        """
        self.driver.delete_all_cookies()

    @allure.step("拼接「{base_url}」和「{url}」")
    def join_url(self, base_url, url):
        if not base_url.endswith('/'):
            base_url += '/'
        if url.startswith('/'):
            url = url[1:]
        return base_url + url

    @allure.step("打开{url}")
    def open_url(self, url):
        try:
            time.sleep(1)
            self.driver.get(url)
            time.sleep(5)
        except ReadTimeoutError as e:
            logger.error(e)
            assert 0, f'打开{url}超时'

    @allure.step("查看当前url地址")
    def get_url(self):
        try:
            return self.driver.current_url
        except Exception as e:
            logger.error(e)
        return ""

    @allure.step("查看当前标题")
    def get_title(self):
        return self.driver.title

    @allure.step("获取「{locator}」中css属性为「css_value」部分内容")
    def get_css_value(self, locator, css_value='background-image', many=False, num=0):
        if many:
            ele = self.find_elements(locator)[num]
        else:
            ele = self.find_element(locator)
        logger.info(f"获取「{locator.desc}」中的「{css_value}」属性内容")
        return ele.value_of_css_property(css_value)

    def page_test(self, url="http://wy.guahao-test.com/"):
        """
        测试单个页面 元素定位是否正确
        :param url:
        :return:
        """
        try:
            self.open(url, None, driver='chrome-h5')
        except Exception:
            pass
        self.refresh()
        for k, v in self._locator_map[self.file_name].items():
            try:
                self.find_element(v)
            except Exception as e:
                logger.error(f'沒有找到{v}:{e}')
        pic_path = f'{TEST_PIC}/{self.file_name}.png'
        self.screenshot_pic(pic_path)

    def add_attribute(self, locator, attributeName, value):
        """
        向页面标签添加新属性
        """
        ele = self.find_element(locator)
        js = "arguments[0].%s=arguments[1]" % attributeName
        self.driver.execute_script(js, ele, value)

    def set_attribute(self, locator, attributeName, value):
        """
        设置页面对象的属性值
        """
        ele = self.find_element(locator)
        js = "arguments[0].setAttribute(arguments[1],arguments[2])"
        self.driver.execute_script(js, ele, attributeName, value)

    def get_attribute(self, locator, attributeName, many=False):
        """
        获取页面对象的属性值
        """
        if many:
            eles = self.find_elements(locator)
            return [ele.get_attribute(attributeName) for ele in eles]
        else:
            ele = self.find_element(locator)
            return ele.get_attribute(attributeName)

    def remove_attribute(self, locator, attributeName):
        """
        删除页面属性
        """
        ele = self.find_element(locator)
        js = "arguments[0].removeAttribute(arguments[1])"
        self.driver.execute_script(js, ele, attributeName)

    @allure.step('弹框提醒等待{t}秒')
    def alert_sleep(self, t, msg=None):
        if not msg:
            msg = f'等待{t}秒'
        self.driver.execute_script(f"window.alert('{msg}');")
        time.sleep(t)
        try:
            alert = self.driver.switch_to.alert
            alert.accept()
        except Exception:
            pass

    @allure.step("隐藏键盘(回车)")
    def hide_keyboard(self):
        try:
            self.driver.hide_keyboard()
        except Exception as e:
            logger.error(f"隐藏键盘失败:{e}")

    @allure.step("键盘操作-{operate_name}")
    def operate_keyboard(self, operate_name, times=1):
        keycode_items = {
            "返回键": 4,
            "回车键": 66,
            "ESC键": 111,
            "退格键": 67,
            "删除键": 112
        }
        for i in range(times):
            try:
                self.driver.press_keycode(keycode_items[operate_name])
            except Exception as e:
                logger.error(f"键盘{operate_name}操作失败：{e}")

    @property
    def width(self):
        """
        获取屏幕宽度
        """
        return self.driver.get_window_size()['width']

    @property
    def height(self):
        """
        获取屏幕高度
        """
        return self.driver.get_window_size()['height']

    @allure.step('从坐标({x1},{y1})滑动到({x2},{y2})')
    def move_ios(self, x1, y1, x2, y2, element=None, duration=0.5):
        self.driver.execute_script("mobile:dragFromToForDuration",
                                   {"duration": duration, "element": element,
                                    "fromX": x1, "fromY": y1,
                                    "toX": x2, "toY": y2}
                                   )

    def move_android(self, way="bottom", size=0.5, base=0.1, duration=1000):
        """
        APP滑动
        """
        x1 = y1 = x2 = y2 = 0
        if way == "left":
            x1 = self.width * (1 - base)
            y1 = self.height * size
            x2 = self.width * base
            y2 = self.height * size
        elif way == "right":
            x1 = self.width * base
            y1 = self.height * size
            x2 = self.width * (1 - base)
            y2 = self.height * size
        elif way == "top":
            x1 = self.width * size
            y1 = self.height * base
            x2 = self.width * size
            y2 = self.height * (1 - base)
        elif way == "bottom":
            x1 = self.width * size
            y1 = self.height * (1 - base)
            x2 = self.width * size
            y2 = self.height * base
        for i in range(2):
            try:
                self.driver.swipe(x1, y1, x2, y2, duration)
                break
            except Exception as e:
                logger.error(e)

    @allure.step("从元素「{origin_loc}」滑动到元素「{destination_loc}」的位置")
    def scroll_android(self, origin_loc, destination_loc, duration=1000):
        # 适用ios
        origin_el = self.find_element(origin_loc)
        destination_el = self.find_element(destination_loc)
        self.driver.scroll(origin_el, destination_el, duration)

    @allure.step("从元素「{origin_el}」滑动到元素「{destination_el}」的位置")
    def scroll_by_webElement(self, origin_el, destination_el, duration=1000):
        # 适用ios
        self.driver.scroll(origin_el, destination_el, duration)

    @allure.step("移动到「{text}」所在的位置")
    def scroll_to_element_with_text_for_android(self, text):
        self.driver.find_element_by_android_uiautomator(
            'new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().text("' + text + '").instance(0));')

    @allure.step("移动到「{text}」所在的位置")
    def scroll_up_until_ele_appear(self, text, sys='android'):
        flag = False
        for i in range(10):
            flag = self.is_text_exists(text, sys=sys)
            if flag:
                break
            else:
                self.move_android(way="bottom", base=0.4)

    @allure.step("点击坐标「{x_size}」「{y_size}」所在的位置")
    def click_point(self, x_size, y_size):
        x = self.width * x_size
        y = self.height * y_size
        self.driver.tap([(x, y)])

    @allure.step("转换参数化元素")
    def convert_element(self, element, value):
        """
        转换参数化元素
        :param element: 原页面元素
               value: 参数化值
        :return: 转化后元素
        """
        el = copy.deepcopy(element)
        el.element = el.element % value
        return el

    @allure.step("获取全部activity")
    def contexts(self):
        return self.driver.contexts

    @allure.step("切换activity到「{context}」")
    def switch_to(self, context):
        self.driver.switch_to.context(context)

    @allure.step("关闭弹框")
    def close_dialog(self, retrytime=1, expect_text: (str, Locator) = "", by_type="mobile_xpath", sys='android',
                     need_break=True, source=''):
        """
        尝试retrytime次，直到expect_text出现
        当出现多个弹框的时候需要增加retrytime的数量
        @param retrytime: 尝试次数
        @param expect_text: 文本
        @param by_type: 元素定位方式
        @param sys: 系统
        @param need_break: 找到某个弹窗后，是否需要跳出循环
        @param source: 处理指定弹窗
        @return:
        """
        for i in range(retrytime):
            if isinstance(expect_text, str):
                if self.is_text_exists(expect_text, sys=sys):
                    break
            elif isinstance(expect_text, Locator):
                if self.has_element(expect_text):
                    break
            self.handler_exception(by_type, sys, need_break, source)

    def handler_exception(self, by_type="mobile_xpath", sys='android', need_break=True, source=''):
        """
        通用弹框处理
        @return:
        """
        _blank_list = [
            Locator("//*[@text='同意']", wait_sec=0, by_type=by_type, desc="同意"),
            Locator("//*[contains(@text,'允许')]", wait_sec=0, by_type=by_type, desc="允许"),
            Locator("//*[@text='确定']", wait_sec=0, by_type=by_type, desc="确定"),
            Locator("//*[@text='稍后再说']", wait_sec=0, by_type=by_type, desc="稍后再说"),
            Locator("//*[@text='以后再说']", wait_sec=0, by_type=by_type, desc="以后再说"),
            Locator("//*[@text='我知道了']", wait_sec=0, by_type=by_type, desc="我知道了"),
            Locator("//*[@text='仅在使用中允许']", wait_sec=0, by_type=by_type, desc="仅在使用中允许"),
            Locator("//*[@text='我知道啦']", wait_sec=0, by_type=by_type, desc="我知道啦"),
            Locator("//*[@text='始终允许']", wait_sec=0, by_type=by_type, desc="始终允许"),
            Locator("//*[@text='仍然视频问诊']", wait_sec=0, by_type=by_type, desc="仍然视频问诊"),
            Locator("//*[@text='知道了']", wait_sec=0, by_type=by_type, desc="知道了")
        ]
        if by_type == 'xpath':
            # H5的弹框关闭
            _blank_list += [
                Locator('//div[@class="wand-dialog-img__close"]', wait_sec=0, by_type=by_type, desc="关闭按钮"),
            ]
        if by_type == "mobile_xpath" and sys == "android":
            # 安卓APP的弹框关闭
            _blank_list += [
                Locator("//android.widget.ImageView[@content-desc='关闭']", wait_sec=0, by_type=by_type, desc="关闭"),

            ]
        if by_type == "mobile_xpath" and sys == "ios":
            # ios APP的弹框关闭
            _blank_list += [
                Locator("//XCUIElementTypeButton[@name='web rontview close']", wait_sec=0, by_type=by_type, desc="关闭"),
                Locator("//XCUIElementTypeImage[@name='icon_consult_coupon_dismiss']", wait_sec=0, by_type=by_type,
                        desc="关闭弹窗"),
                Locator("//XCUIElementTypeButton[@name='好']", wait_sec=0, by_type=by_type, desc="权限弹窗-好"),
                Locator("//XCUIElementTypeButton[@name='取消']", wait_sec=0, by_type=by_type, desc="权限弹窗-取消"),
                Locator("//XCUIElementTypeButton[@name='以后']", wait_sec=0, by_type=by_type, desc="权限弹窗-以后"),

            ]
        if source == 'local':
            _blank_list += [
                # 需要区分场景（有些页面图片可点击，导致误点）
                Locator("//*[@resource-id='android:id/content']//android.widget.ImageView", wait_sec=0, by_type=by_type,
                        desc="浮层"),
            ]
        logger.info("+++++++++++++通用弹框处理+++++++++++++")
        for index, loc in enumerate(_blank_list):
            try:
                self.click(loc)
                logger.info(f"找到了 {loc},并且已点击")
                if need_break:
                    if index != len(_blank_list) - 1:
                        self.has_element(_blank_list[index + 1])
                        continue
                    else:
                        break
            except Exception as e:
                e = str(e).replace('\n', '')
                logger.info(f"未找到『{loc.desc}』:{e}")
                pass


class AccessCode(object):
    """
    H5极验操作
    """

    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 20)
        self.border = 6  # 设置偏差值

    def save_img(self, img_name, class_name):
        """

        :param img_name: 保存图片的名字
        :param class_name: 需要保存的canvas的className
        :return:
        """
        getImgJS = 'return document.getElementsByClassName("' + class_name + '")[0].toDataURL("image/png");'
        img = self.driver.execute_script(getImgJS)
        base64_data_img = img[img.find(',') + 1:]
        image_base = base64.b64decode(base64_data_img)
        file = open(img_name, 'wb')
        file.write(image_base)
        file.close()

    def get_track(self, distance):
        """
        根据偏移量获取移动轨迹
        :param distance: 偏移量
        :return: 移动轨迹
        """
        # 移动轨迹
        track = []
        # 当前位移
        current = 0
        # 减速阈值
        mid = distance * 4 / 5
        # 计算间隔
        t = 0.2
        # 初速度
        v = 0
        while current < distance:
            if current < mid:
                # 加速度为正2
                a = 2
            else:
                # 加速度为负3
                a = -3
            # 初速度v0
            v0 = v
            # 当前速度v = v0 + at
            v = v0 + a * t
            # 移动距离x = v0t + 1/2 * a * t^2
            move = v0 * t + 1 / 2 * a * t * t
            # 当前位移
            current += move
            # 加入轨迹
            track.append(round(move))
        return track

    def move_to_gap(self, slider, track):
        """
        拖动滑块到缺口处
        :param slider: 滑块
        :param track: 轨迹
        :return:
        """
        ActionChains(self.driver).click_and_hold(slider).perform()
        for x in track:
            ActionChains(self.driver).move_by_offset(xoffset=x, yoffset=0).perform()
            time.sleep(0.1 * random.random())
        ActionChains(self.driver).release().perform()

    def get_slider(self):
        """
        获取滑块
        :return: 滑块对象
        """
        slider = self.wait.until(expected_conditions.element_to_be_clickable((By.CLASS_NAME, 'geetest_slider_button')))
        return slider

    def is_similar_color(self, x_pixel, y_pixel):
        """
        判断颜色是否相近
        :param x_pixel:
        :param y_pixel:
        :return:
        """
        for i, pixel in enumerate(x_pixel):
            if abs(y_pixel[i] - pixel) > 50:
                return False
        return True

    def get_offset_distance(self, cut_image, full_image):
        """
        计算距离
        :param cut_image:
        :param full_image:
        :return:
        """
        for x in range(cut_image.width):
            for y in range(cut_image.height):
                cpx = cut_image.getpixel((x, y))
                fpx = full_image.getpixel((x, y))
                if not self.is_similar_color(cpx, fpx):
                    img = cut_image.crop((x, y, x + 50, y + 40))
                    # 保存一下计算出来位置图片，看看是不是缺口部分
                    img.save(f'{TEST_PIC}/gap.png')
                    attach_png(f'{TEST_PIC}/gap.png', '极验滑块图片')
                    return x

    def crack(self):
        """
        验证操作
        :return:
        """
        full_path = f'{TEST_PIC}/full.jpg'
        cut_path = f'{TEST_PIC}/cut.jpg'
        # 保存原始图片
        self.save_img(full_path, 'geetest_canvas_fullbg')
        attach_png(full_path, '极验原始图片')
        # 保存缺口图片
        self.save_img(cut_path, 'geetest_canvas_bg')
        attach_png(cut_path, '极验缺口图片')
        full_image = Image.open(full_path)
        cut_image = Image.open(cut_path)
        # 计算滑动距离
        distance = self.get_offset_distance(cut_image, full_image)
        # 减去缺口位移
        distance -= self.border
        # 获取滑块对象
        slider = self.get_slider()
        # 模拟人为滑动轨迹
        track = self.get_track(distance)
        # 拖动滑块
        self.move_to_gap(slider, track)
        time.sleep(2)
        try:
            track = self.get_track(distance - 65)
            self.move_to_gap(slider, track)
        except Exception:
            pass


if __name__ == '__main__':
    registered = Locator(
        element='//p[text()="挂号"]',
        wait_sec=3,
        by_type="xpath",
        locator_name="registered",
        desc="挂号")
    print(registered)  # 挂号:(By:xpath,element://p[text()="挂号"])
